-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Improve protocol-level handshaking protocol #3049
Conversation
@MarkusTeufelberger, I would appreciate a review from you as well, since this also serves to correct several issues you've previously identified or mentioned in passing, including updating documentation. |
Jenkins Build SummaryBuilt from this commit Built at 20190823 - 16:04:16 Test Results
|
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.
Phew, that took much longer to read through and understand than expected.
Thanks for the PR!
I'd really like to see at least some options besides signing the encoded TLS "finished" messages available. RFC5705 sounds more reasonable to me, or also just offering to use actual widely trusted certificates, if available (e.g. via Let's Encrypt).
return hdr; | ||
} | ||
|
||
// Version 2 header: compressed payload. |
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 simpler(?) solution might be to add a "header" message type in Protobuf that encodes all these parameters (payload_wire_size, payload_real_size, compression_algorithm) and only have a "header_wire_size" parameter to delimit stuff on the wire (might be possible to just use 1-2 bytes of length data).
You'd then send header_wire_size | header_proto | payload_proto
on the wire. You can add the actual version of the protocol used to the header_proto as well to be more flexible and have more options to extend it in the future.
The code below just seems a bit like re-inventing yet another wire protocol, even though you are already using protobuf.
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 hear you. I actually don't really like this binary prefix business, and am seriously considering migrating to a pure WebSocket-based transport (which would make the binary header unnecessary) or adopting an approach closer to what you describe (involving coded protobuf streams).
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.
gRPC might be worth a look too then...
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 that gRPC is probably overkill and will require more rearchitecting than I'd like to do at this time. But I will take a look.
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 just started looking into libp2p as well (used by Ethereum 2.0, IPFS etc.), but there's no C++ implementation so far and I suspect they are also vulnerable to active MITM with their TLS handshake.
Cap'n'Proto might also be worth a look, gRPC has the advantage that it might also take care of the transport stuff and is probably more widely used and implemented in other software. Wouldn't solve the MITM handshake mitigation, but there were options discussed for that already.
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, libp2p's handshake is not vulnerable and also prevents MITM attacks with a standard certificate based approach. The only extra part is that one needs to check that the host key embedded in the certificate did actually sign the public key of the certificate (and that the connection is using this key). That's more related to establishing a connection though, not so much to the stream of protobuf messages 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.
While you can certainly address this issue using certificates, I still think the extra work that users have to go through makes it unlikely that they will. The primary objection to the existing MITM mitigation technique seems to be "yuck, I don't like how this is done." And that's fine but it's not really disposative.
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.
No, it is "this is not possible without adding a patch to Python's stdlib" in my case. Accessing these messages is just not possible in a lot of implementations of TLS, verifying a special field in a certificate is.
The certs in libp2p are self-signed and can be created by the application ad-hoc, so there's no work on the user's end...
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.
Hmm. This actually does give me an idea. I may do this, but it'll have to wait for a follow-up PR.
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.
Based on what was described, my vote would be for the "header" message type & header_wire_size, seems like a simple solution that does the job. Since 'finished' and 'peer_finished' messages are part of the public openssl API, higher level wrappers should expose this functionality, if they do not we can push for this as a community (it's FOSS afterall!).
Reading about Cap'n'Proto, it seems cool though is a binary protocol which has downsides (additional debugging complexity, etc). Didn't look into libp2p as @MarkusTeufelberger said there was no C++ implementation. Self-signed certs would work but introduce additional management complexity (yes rippled could implement it internally but so would each protocol implementation). And what would we get beyond what is already provided in that case?
Just jumping in here as I start to take a look at this. We've already implemented the handshake protocol in the ruby library and I'd like to keep that up to date. Reading RFC5705, it specified the necessity for a 'master_secret' and 'label' for the EKM context, what were you thinking for those? I'd personally vote against certificates as it's a centralizing component and the whole point on the P2P protocol is to be decentralized. |
master_secret and label can be public values as far as I understand it. Certificates don't have to be signed by a CA, they can be self-signed. Take a look at libp2p's TLS 1.3 handshake protocol for example. Nothing centralized about that one. |
That's neat. Even if we implement something different, it's likely that this mechanism will stick around for a while, and you'll have plenty of time to implement it. Also, hopefully it'll be something that doesn't require custom ruby gems.
I'm not convinced the RFC5705 solution(s) prevent the kind of attack that our simple construct prevents anyways. I have technique that can do it and which doesn't require reaching into the SSL session and pulling something out of it, but it's a more complicated process than what we do currently requiring at least 5 messages (3 in one direction, 2 in the other direction) to be exchanged.
I think that we could implement something where the server creates a self-signed certificate (if none exists) and it would be no more difficult for the user, but I need to carefully consider the ramifications and ensure that we get the security guarantees we want. |
Codecov Report
@@ Coverage Diff @@
## develop #3049 +/- ##
===========================================
+ Coverage 68.94% 69.08% +0.14%
===========================================
Files 688 690 +2
Lines 55743 55636 -107
===========================================
+ Hits 38431 38436 +5
+ Misses 17312 17200 -112
Continue to review full report at Codecov.
|
|
||
// Version 1 header: uncompressed payload. | ||
// The top six bits of the first byte are 0. | ||
if ((*iter & 0xFC) == 0) |
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.
Any reason as to use the top 6 bits of the first byte (part of the payload_wire_size field) for the compression flag? Is it to maintain compatibility w/ the current header format (size)?
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 certainly do that. I have a different plan for compression in mind though and I'll probably repurpose the high bit to indicate compression when I do that PR.
Great job @nbougalis, this certainly looks like it sets the stage for some awesome changes when it comes to compression! As @MarkusTeufelberger said it was a little tricky to review this one though due to all the features / changes spread out in the many patches.
|
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.
@nbougalis I like these changes a lot. I did a pass for nits/language-level feedback. I still need to do a pass and think about the meat of these changes.
msg.insert("Server", BuildInfo::getFullVersionString()); | ||
msg.insert("Remote-Address", remote_address.to_string()); | ||
msg.insert(boost::beast::http::field::connection, "close"); | ||
msg.body() = text; | ||
msg.prepare_payload(); | ||
return std::make_shared<SimpleWriter>(msg); |
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.
SimpleWriter
is also deprecated, tho I don't know what replaces it (and obviously not part of this patch).
Just FYI, VS doesn't like the |
This commit works around the compile error I pointed out above by doing it "the hard way" for MSVC builds. ximinez@c974971 |
} | ||
}; | ||
|
||
BEAST_DEFINE_TESTSUITE(TMHello,overlay,ripple); |
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 always reluctant to get rid of unit tests without a really good reason. I don't think you've entirely removed the functionality being tested here, so why remove the tests?
} | ||
}; | ||
|
||
BEAST_DEFINE_TESTSUITE(BuildInfo,ripple_data,ripple); |
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 above, I am always reluctant to get rid of unit tests without a really good reason. I don't think you've entirely removed the functionality being tested here, so why remove the tests?
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 code changes ProtocolVersion
to be a simple std::pair
and all the relevant operators that were being tested here before are now provided by std so I don't believe that testing that functionality is necessary.
The only test that is no longer there is BEAST_EXPECT(current_protocol >= minimum_protocol)
and the new implementation of this code simply doesn't have such values in a way that makes sense to test. Rather the implementation requires that all supported versions are listed (in sorted order, with no duplicates) in the supportedProtocolList
array.
I had a static_assert
that was meant to test the sorted & no dupes requirement, but it turns out that it was broken and I didn't know because I had incorrectly gated it so that it would require compiling with C++20. So your comment did help me find that, thanks!
I rewrote the static_assert
to execute under C++17 and verified that it works correctly.
This commit restructures the HTTP based protocol negotiation that `rippled` executes and introduces support for negotiation of compression for peer links which, if implemented, should result in significant bandwidth savings for some server roles. This commit also introduces the new `[network_id]` configuration option that administrators can use to specify which network the server is part of and intends to join. This makes it possible for servers from different networks to drop the link early. The changeset also improves the log messages generated when negotiation of a peer link upgrade fails. In the past, no useful information would be logged, making it more difficult for admins to troubleshoot errors. This commit also fixes RIPD-237 and RIPD-451
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 haven't done a full review, but did check out the more recently added commits.
I also fixed the Windows build by ripping off the boost::function_output_iterator
with an iterator that skips the lambda and does the assignment directly. It's probably not the best option, but it works.
Edit: The branch is https://github.com/ximinez/rippled/tree/nb--handshake
The change in question is: ximinez@878b1d8 I appreciate you doing this, but it seems to me that it is overkill to address what is, at the end of the day, either a compiler deficiency or an issue with Boost against Windows. The fix that I put in place after your initial report (creating a |
Welp, egg on my face, because I'm having no problems building at all now. I have no idea what changed, but appears to be a moot point either way. |
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 went through the whole thing this time, and it looks good. I have a couple of very minor and optional suggestions.
I notice there's a CI failure building the validator keys tool, which is because KeyType.h got moved. I think that'll have to be addressed separately, but probably before this change is released.
This commit restructures the HTTP based protocol negotiation that
rippled
executes and introduces support for negotiation of compression for peer links which, if implemented, should result in significant bandwidth savings for some server roles.This commit also introduces the new
[network_id]
configuration option that administrators can use to specify which network the server is part of and intends to join. This makes it possible for servers from different networks to drop the link early.The changeset also improves the log messages generated when negotiation of a peer link upgrade fails. In the past, no useful information would be logged, making it more difficult for admins to troubleshoot errors.