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

Improve MITM detection during session establishment: #4395

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/ripple/app/main/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ class ApplicationImp : public Application, public BasicApp
NodeCache m_tempNodeCache;
CachedSLEs cachedSLEs_;
std::pair<PublicKey, SecretKey> nodeIdentity_;
std::string nodePublicIdentity_;

ValidatorKeys const validatorKeys_;

std::unique_ptr<Resource::Manager> m_resourceManager;
Expand Down Expand Up @@ -590,6 +592,12 @@ class ApplicationImp : public Application, public BasicApp
return nodeIdentity_;
}

std::string const&
getNodePublicIdentity() const override
{
return nodePublicIdentity_;
}

PublicKey const&
getValidationPublicKey() const override
{
Expand Down Expand Up @@ -1276,6 +1284,7 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
m_orderBookDB.setup(getLedgerMaster().getCurrentLedger());

nodeIdentity_ = getNodeIdentity(*this, cmdline);
nodePublicIdentity_ = toBase58(TokenType::NodePublic, nodeIdentity().first);

if (!cluster_->load(config().section(SECTION_CLUSTER_NODES)))
{
Expand Down
3 changes: 3 additions & 0 deletions src/ripple/app/main/Application.h
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ class Application : public beast::PropertyStream::Source
virtual std::pair<PublicKey, SecretKey> const&
nodeIdentity() = 0;

virtual std::string const&
getNodePublicIdentity() const = 0;

virtual PublicKey const&
getValidationPublicKey() const = 0;

Expand Down
1 change: 1 addition & 0 deletions src/ripple/basics/base_uint.h
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ class base_uint
using uint128 = base_uint<128>;
using uint160 = base_uint<160>;
using uint256 = base_uint<256>;
using uint512 = base_uint<512>;

template <std::size_t Bits, class Tag>
inline int
Expand Down
90 changes: 48 additions & 42 deletions src/ripple/overlay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ Network-ID: 1
Network-Time: 619234489
Public-Key: n94MvLTiHQJjByfGZzvQewTxQP2qjF6shQcuHwCjh5WoiozBrdpX
Session-Signature: MEUCIQCOO8tHOh/tgCSRNe6WwOwmIF6urZ5uSB8l9aAf5q7iRAIgA4aONKBZhpP5RuOuhJP2dP+2UIRioEJcfU4/m4gZdYo=
Session-EKM-Signature: 4F73F4DF23453250BDDF81AFBB5CBF0FD32B92AAF548331DB0ECF1DB988BE2F3D7264621FC1E93404B77612F235AC6FFD4F1B7B4D420E8071384273D9A60201D
Remote-IP: 192.0.2.79
Closed-Ledger: llRZSKqvNieGpPqbFGnm358pmF1aW96SDIUQcnMh6HI=
Previous-Ledger: q4aKbP7sd5wv+EXArwCmQiWZhq9AwBl2p/hCtpGJNsc=
Closed-Ledger: 4F73F45E95343F7446F91B5F70E3AE4E155CCAA2B1CF2F267C99FD39C1C24178
Previous-Ledger: F9D75AAB7D975BADC6D008C4AE94D9B7B6AA323EEE4B133F5B75CE92445C88ED
```

##### Example HTTP Upgrade Response (Success)
Expand Down Expand Up @@ -260,18 +261,32 @@ under the specified domain and locating the public key of this server under the
Sending a malformed domain will prevent a connection from being established.


| Field Name | Request | Response |
|-------------------------|:-----------------: |:-----------------: |
| `Session-EKM-Signature` | :heavy_check_mark: | :heavy_check_mark: |

The `Session-EKM-Signature` field supersedes the `Session-Signature` field and is
mandatory if `Session-Signature` is not present. It is used to secure the peer
link against certain types of attack. For more details see the section titled
"Session Security" below.

The value is specified in **HEX** encoding.


| Field Name | Request | Response |
|--------------------- |:-----------------: |:-----------------: |
| `Session-Signature` | :heavy_check_mark: | :heavy_check_mark: |

The `Session-Signature` field is mandatory and is used to secure the peer link
against certain types of attack. For more details see "Session Signature" below.
The `Session-Signature` field is a legacy field that has been superseded by the
`Session-EKM-Signature` field. It will be removed in a future release of the
software.

It is used to secure the peer link against certain types of attack. For more
details see the section titled "Session Signature" below.
Comment on lines +284 to +285
Copy link
Collaborator

Choose a reason for hiding this comment

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

There is no section titled "Session Signature" anymore. Should probably be "Session Security".


The value is presently encoded using **Base64** encoding, but implementations
should support both **Base64** and **HEX** encoding for this value.

For more details on this field, please see **Session Signature** below.


| Field Name | Request | Response |
|--------------------- |:-----------------: |:-----------------: |
Expand All @@ -298,16 +313,16 @@ considers to be closed.

The value is encoded as **HEX**, but implementations should support both
**Base64** and **HEX** encoding for this value for legacy purposes.

| Field Name | Request | Response |
|--------------------- |:-----------------: |:-----------------: |
| `Previous-Ledger` | :white_check_mark: | :white_check_mark: |

If present, identifies the hash of the parent ledger that the sending server
considers to be closed.

The value is presently encoded using **Base64** encoding, but implementations
should support both **Base64** and **HEX** encoding for this value.
The field data should be encoded using **HEX**, but implementations should
correctly interpret both **Base64** and **HEX** encodings.

#### Additional Headers

Expand All @@ -318,35 +333,31 @@ Implementations should not reject requests because of the presence of fields
that they do not understand.


### Session Signature
### Session Security

Even for SSL/TLS encrypted connections, it is possible for an attacker to mount
relatively inexpensive MITM attacks that can be extremely hard to detect and
may afford the attacker the ability to intelligently tamper with messages
exchanged between the two endpoints.

This risk can be mitigated if at least one side has a certificate from a certificate
authority trusted by the other endpoint, but having a certificate is not always
possible (or even desirable) in a decentralized and permissionless network.
This risk can be mitigated if at least one side has a certificate from a CA that
is trusted by the other endpoint, but having a certificate is not always
possible (or, indeed, desirable) in a decentralized and permissionless network.

Ultimately, the goal is to ensure that two endpoints A and B know that they are
talking directly to each other over a single end-to-end SSL/TLS session instead
of two separate SSL/TLS sessions, with an attacker acting as a proxy.

The XRP Ledger protocol prevents this attack by leveraging the fact that the two
servers each have a node identity, in the form of **`secp256k1`** keypairs, and
use that to strongly bind the SSL/TLS session to the node identities of each of
the two servers at the end of the SSL/TLS session.
use that, along with a secure fingerprint associated with the SSL/TLS session to
strongly bind the SSL/TLS session to the node identities of each of the servers
at the end of the SSL/TLS session.

To do this we "reach into" the SSL/TLS session, and extract the **`finished`**
messages for the local and remote endpoints, and combine them to generate a unique
"fingerprint". By design, this fingerprint should be the same for both SSL/TLS
endpoints.

That fingerprint, which is never shared over the wire (since each endpoint will
calculate it independently), is then signed by each server using its public
**`secp256k1`** node identity and the signature is transferred over the SSL/TLS
encrypted link during the protocol handshake phase.
The fingerprint is never shared over the wire (the two endpoints calculate it
independently) and is signed by each server using its public **`secp256k1`**
node identity. The resulting signature is transferred over the SSL/TLS session
during the protocol handshake phase.

Each side of the link will verify that the provided signature is from the claimed
public key against the session's unique fingerprint. If this signature check fails
Expand All @@ -365,17 +376,12 @@ message stream between Alice and Bob, although she may be still be able to injec
delays or terminate the link.


# Ripple Clustering #

A cluster consists of more than one Ripple server under common
administration that share load information, distribute cryptography
operations, and provide greater response consistency.
# Clustering #

Cluster nodes are identified by their public node keys. Cluster nodes
exchange information about endpoints that are imposing load upon them.
Cluster nodes share information about their internal load status. Cluster
nodes do not have to verify the cryptographic signatures on messages
received from other cluster nodes.
A cluster consists of several servers, typically under common administration,
that are configured to work cooperatively by sharing server load information,
details about shards, optimizing processing to avoid duplicating work that other
cluster members have done, and more.

## Configuration ##

Expand All @@ -385,9 +391,9 @@ beginning with the letter `n`. The key is maintained across runs in a
database.

Cluster members are configured in the `rippled.cfg` file under
`[cluster_nodes]`. Each member should be configured on a line beginning
with the node public key, followed optionally by a space and a friendly
name.
`[cluster_nodes]`. Each member should be configured on a separate line,
beginning with its node public key, followed optionally by a space and a
friendly name.

Because cluster members can introduce other cluster members, it is not
necessary to configure every cluster member on every other cluster member.
Expand All @@ -413,11 +419,11 @@ not relay a transaction with an incorrect signature. Validators may wish to
disable this feature, preferring the additional load to get the additional
security of having validators check each transaction.

Local checks for transaction checking are also bypassed. For example, a
server will not reject a transaction from a cluster peer because the fee
does not meet its current relay fee. It is preferable to keep the cluster
in agreement and permit confirmation from one cluster member to more
reliably indicate the transaction's acceptance by the cluster.
Several "local" checks are also bypassed. For example, a server will not reject
a transaction from a cluster peer because the fee does not meet its current
relay fee. It is preferable to keep the cluster in agreement and permit
confirmation from one cluster member to more reliably indicate the transaction's
acceptance by the cluster.

## Server Load Information ##

Expand Down
44 changes: 36 additions & 8 deletions src/ripple/overlay/impl/ConnectAttempt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -197,26 +197,32 @@ ConnectAttempt::onHandshake(error_code ec)
slot_, beast::IPAddressConversion::from_asio(local_endpoint)))
return fail("Duplicate connection");

auto const sharedValue = makeSharedValue(*stream_ptr_, journal_);
if (!sharedValue)
return close(); // makeSharedValue logs

req_ = makeRequest(
!overlay_.peerFinder().config().peerPrivate,
app_.config().COMPRESSION,
app_.config().LEDGER_REPLAY,
app_.config().TX_REDUCE_RELAY_ENABLE,
app_.config().VP_REDUCE_RELAY_ENABLE);

auto const sharedValue = makeSharedValue(*stream_ptr_, journal_);
if (!sharedValue)
return close(); // makeSharedValue logs

auto const ekm = getSessionEKM(*stream_ptr_, app_.instanceID(), true);
if (!ekm)
return fail("Unable to retrieve EKM for session");

buildHandshake(
req_,
*sharedValue,
*ekm,
overlay_.setup().networkID,
overlay_.setup().public_ip,
remote_endpoint_.address(),
app_);

setTimer();

boost::beast::http::async_write(
stream_,
req_,
Expand Down Expand Up @@ -347,15 +353,37 @@ ConnectAttempt::processResponse()
"processResponse: Unable to negotiate protocol version");
}

auto const sharedValue = makeSharedValue(*stream_ptr_, journal_);
if (!sharedValue)
return close(); // makeSharedValue logs

try
{
auto const sharedValue = makeSharedValue(*stream_ptr_, journal_);
if (!sharedValue)
return close(); // makeSharedValue logs

auto const peerInstanceID = [this]() {
std::uint64_t iid = 0;

if (auto const iter = response_.find("Instance-Cookie");
iter != response_.end())
{
if (!beast::lexicalCastChecked(iid, iter->value().to_string()))
throw std::runtime_error("Invalid instance cookie");

if (iid == 0)
throw std::runtime_error("Invalid instance cookie");
}

return iid;
}();

auto const ekm = getSessionEKM(*stream_ptr_, peerInstanceID, false);

if (!ekm)
return fail("Unable to retrieve EKM for session");

auto publicKey = verifyHandshake(
response_,
*sharedValue,
*ekm,
overlay_.setup().networkID,
overlay_.setup().public_ip,
remote_endpoint_.address(),
Expand Down
Loading