mirror of
https://github.com/Xahau/xahaud.git
synced 2025-11-19 10:05:48 +00:00
Harden MITM detection during session establishment:
Even with TLS encrypted connections, it is possible for a determined attacker to mount certain types of relatively easy man-in-the-middle attacks which, if successful, could allow an attacker to tamper with messages exchanged between endpoints. The risk can be mitigated if each side has a certificate issued by a CA that the other side trusts. In the context of a decentralized and permissionless network, this is neither reasonable nor desirable. To prevent this problem all we need is to allow the two endpoints, A and B, to be able to independently verify that they are connected to each other over a single end-to-end TLS session, instead of separate TLS sessions which the attacker bridges. The protocol level handshake implements this security check by using digital signatures: each endpoint derives a fingerprint from the TLS session, which it signs with the private key associated with its own node identity. This strongly binds the TLS session to the identities of the two endpoints of the session. This commit introduces a new fingerprint derivation that uses modern and standardized TLS exporter functionality, instead of the existing derivation whch uses OpenSSL APIs that are non-standard, and derives different "incoming" and "outgoing" security cookies. Lastly, this commit refines the "self-connection" check to allow for the detection of accidental instances of node identity sharing. This check was first introduced with #4195 but was partially reverted due to a bug with #4438. By using distinct security cookies for incoming and outgoing connections, an attacker is no longer able to claim the identity of its peer by echoing its security cookie. The change is backwards compatible and servers with this commit will still generate and verify old-style fingerprints, in addition to the new style fingerprints. For a fuller discussion on this topic, please see: https://github.com/openssl/openssl/issues/5509 https://github.com/ripple/rippled/issues/2413 This commit was previously introduced as #3929, which was closed. If merged, it also fixes #2413 (which had been closed as a 'WONTFIX').
This commit is contained in:
@@ -186,6 +186,8 @@ public:
|
||||
NodeCache m_tempNodeCache;
|
||||
CachedSLEs cachedSLEs_;
|
||||
std::pair<PublicKey, SecretKey> nodeIdentity_;
|
||||
std::string nodePublicIdentity_;
|
||||
|
||||
ValidatorKeys const validatorKeys_;
|
||||
|
||||
std::unique_ptr<Resource::Manager> m_resourceManager;
|
||||
@@ -591,6 +593,12 @@ public:
|
||||
return nodeIdentity_;
|
||||
}
|
||||
|
||||
std::string const&
|
||||
getNodePublicIdentity() const override
|
||||
{
|
||||
return nodePublicIdentity_;
|
||||
}
|
||||
|
||||
PublicKey const&
|
||||
getValidationPublicKey() const override
|
||||
{
|
||||
@@ -1274,6 +1282,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)))
|
||||
{
|
||||
|
||||
@@ -242,6 +242,9 @@ public:
|
||||
virtual std::pair<PublicKey, SecretKey> const&
|
||||
nodeIdentity() = 0;
|
||||
|
||||
virtual std::string const&
|
||||
getNodePublicIdentity() const = 0;
|
||||
|
||||
virtual PublicKey const&
|
||||
getValidationPublicKey() const = 0;
|
||||
|
||||
|
||||
@@ -548,6 +548,7 @@ public:
|
||||
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>
|
||||
[[nodiscard]] inline constexpr std::strong_ordering
|
||||
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
|
||||
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 |
|
||||
|--------------------- |:-----------------: |:-----------------: |
|
||||
@@ -306,8 +321,8 @@ The value is encoded as **HEX**, but implementations should support both
|
||||
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
|
||||
|
||||
@@ -318,16 +333,16 @@ 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
|
||||
@@ -335,18 +350,14 @@ 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
|
||||
@@ -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 #
|
||||
# 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.
|
||||
|
||||
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 ##
|
||||
|
||||
@@ -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.
|
||||
@@ -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 ##
|
||||
|
||||
|
||||
@@ -197,10 +197,6 @@ 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,
|
||||
@@ -208,15 +204,25 @@ ConnectAttempt::onHandshake(error_code ec)
|
||||
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_,
|
||||
@@ -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(),
|
||||
|
||||
@@ -26,12 +26,8 @@
|
||||
#include <ripple/overlay/impl/Handshake.h>
|
||||
#include <ripple/protocol/digest.h>
|
||||
#include <boost/regex.hpp>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
// VFALCO Shouldn't we have to include the OpenSSL
|
||||
// headers or something for SSL_get_finished?
|
||||
|
||||
namespace ripple {
|
||||
|
||||
std::optional<std::string>
|
||||
@@ -118,10 +114,12 @@ makeFeaturesResponseHeader(
|
||||
- `SSL_get_peer_finished`.
|
||||
@return `true` if successful, `false` otherwise.
|
||||
|
||||
@note This construct is non-standard. There are potential "standard"
|
||||
alternatives that should be considered. For a discussion, on
|
||||
this topic, see https://github.com/openssl/openssl/issues/5509 and
|
||||
https://github.com/ripple/rippled/issues/2413.
|
||||
@deprecated This construct is non-standard and is now deprecated in favor
|
||||
of using `SSL_export_keying_material`. Support for this will
|
||||
be removed in a future version of the codebase.
|
||||
For a fuller discussion on this topic, please see:
|
||||
https://github.com/openssl/openssl/issues/5509
|
||||
https://github.com/ripple/rippled/issues/2413.
|
||||
*/
|
||||
static std::optional<base_uint<512>>
|
||||
hashLastMessage(SSL const* ssl, size_t (*get)(const SSL*, void*, size_t))
|
||||
@@ -136,11 +134,33 @@ hashLastMessage(SSL const* ssl, size_t (*get)(const SSL*, void*, size_t))
|
||||
|
||||
sha512_hasher h;
|
||||
|
||||
base_uint<512> cookie;
|
||||
uint512 cookie;
|
||||
SHA512(buf, len, cookie.data());
|
||||
return cookie;
|
||||
}
|
||||
|
||||
std::optional<uint256>
|
||||
getSessionEKM(stream_type& ssl, std::uint64_t instance, bool outgoing)
|
||||
{
|
||||
std::string const label = std::string("XRPL-EKM-COOKIE:V1:") +
|
||||
std::to_string(instance) + (outgoing ? ":OUT" : ":IN");
|
||||
|
||||
uint256 km;
|
||||
|
||||
if (SSL_export_keying_material(
|
||||
ssl.native_handle(),
|
||||
km.data(),
|
||||
km.size(),
|
||||
label.data(),
|
||||
label.size(),
|
||||
nullptr,
|
||||
0,
|
||||
0) == 1)
|
||||
return km;
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<uint256>
|
||||
makeSharedValue(stream_type& ssl, beast::Journal journal)
|
||||
{
|
||||
@@ -176,7 +196,8 @@ makeSharedValue(stream_type& ssl, beast::Journal journal)
|
||||
void
|
||||
buildHandshake(
|
||||
boost::beast::http::fields& h,
|
||||
ripple::uint256 const& sharedValue,
|
||||
uint256 const& sharedValue,
|
||||
uint256 const& ekm,
|
||||
std::optional<std::uint32_t> networkID,
|
||||
beast::IP::Address public_ip,
|
||||
beast::IP::Address remote_ip,
|
||||
@@ -194,9 +215,7 @@ buildHandshake(
|
||||
"Network-Time",
|
||||
std::to_string(app.timeKeeper().now().time_since_epoch().count()));
|
||||
|
||||
h.insert(
|
||||
"Public-Key",
|
||||
toBase58(TokenType::NodePublic, app.nodeIdentity().first));
|
||||
h.insert("Public-Key", app.getNodePublicIdentity());
|
||||
|
||||
{
|
||||
auto const sig = signDigest(
|
||||
@@ -204,6 +223,11 @@ buildHandshake(
|
||||
h.insert("Session-Signature", base64_encode(sig.data(), sig.size()));
|
||||
}
|
||||
|
||||
h.insert(
|
||||
"Session-EKM-Signature",
|
||||
strHex(signDigest(
|
||||
app.nodeIdentity().first, app.nodeIdentity().second, ekm)));
|
||||
|
||||
h.insert("Instance-Cookie", std::to_string(app.instanceID()));
|
||||
|
||||
if (!app.config().SERVER_DOMAIN.empty())
|
||||
@@ -225,7 +249,8 @@ buildHandshake(
|
||||
PublicKey
|
||||
verifyHandshake(
|
||||
boost::beast::http::fields const& headers,
|
||||
ripple::uint256 const& sharedValue,
|
||||
uint256 const& sharedValue,
|
||||
uint256 const& ekm,
|
||||
std::optional<std::uint32_t> networkID,
|
||||
beast::IP::Address public_ip,
|
||||
beast::IP::Address remote,
|
||||
@@ -286,7 +311,7 @@ verifyHandshake(
|
||||
throw std::runtime_error("Peer clock is too far off");
|
||||
}
|
||||
|
||||
PublicKey const publicKey = [&headers] {
|
||||
PublicKey const pubKey = [&headers] {
|
||||
if (auto const iter = headers.find("Public-Key"); iter != headers.end())
|
||||
{
|
||||
auto pk = parseBase58<PublicKey>(
|
||||
@@ -310,20 +335,67 @@ verifyHandshake(
|
||||
// private key corresponding to the public node identity it claims.
|
||||
// 2) it verifies that our SSL session is end-to-end with that node
|
||||
// and not through a proxy that establishes two separate sessions.
|
||||
//
|
||||
// Note that if both EKM and legacy style sessions signatures are present
|
||||
// both must be correct.
|
||||
bool hasEKM = false;
|
||||
|
||||
{
|
||||
auto const iter = headers.find("Session-Signature");
|
||||
bool ok = false;
|
||||
|
||||
if (iter == headers.end())
|
||||
throw std::runtime_error("No session signature specified");
|
||||
if (auto h = headers.find("Session-EKM-Signature");
|
||||
h != headers.end() && !h->value().empty())
|
||||
{
|
||||
if (auto const sig = strUnHex(h->value().to_string()))
|
||||
{
|
||||
if (!verifyDigest(pubKey, ekm, {sig->data(), sig->size()}))
|
||||
throw std::runtime_error("Failed to verify session (EKM)");
|
||||
|
||||
auto sig = base64_decode(iter->value().to_string());
|
||||
hasEKM = true;
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!verifyDigest(publicKey, sharedValue, makeSlice(sig), false))
|
||||
throw std::runtime_error("Failed to verify session");
|
||||
if (auto h = headers.find("Session-Signature"); h != headers.end())
|
||||
{
|
||||
if (auto sig = base64_decode(h->value().to_string()); !sig.empty())
|
||||
{
|
||||
if (!verifyDigest(pubKey, sharedValue, makeSlice(sig), false))
|
||||
throw std::runtime_error("Failed to verify session");
|
||||
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ok)
|
||||
throw std::runtime_error("Advanced MITM checks not present");
|
||||
}
|
||||
|
||||
if (publicKey == app.nodeIdentity().first)
|
||||
throw std::runtime_error("Self connection");
|
||||
if (pubKey == app.nodeIdentity().first)
|
||||
{
|
||||
auto const peerInstanceID = [&headers]() {
|
||||
std::uint64_t iid = 0;
|
||||
|
||||
if (auto const iter = headers.find("Instance-Cookie");
|
||||
iter != headers.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;
|
||||
}();
|
||||
|
||||
// When EKM is supported, we can be confident that the remote endpoint
|
||||
// has the same node private key as us.
|
||||
if (hasEKM && peerInstanceID != app.instanceID())
|
||||
app.signalStop("Another server is using our node identity");
|
||||
|
||||
throw std::runtime_error("Self-connection detected");
|
||||
}
|
||||
|
||||
if (auto const iter = headers.find("Local-IP"); iter != headers.end())
|
||||
{
|
||||
@@ -361,7 +433,7 @@ verifyHandshake(
|
||||
}
|
||||
}
|
||||
|
||||
return publicKey;
|
||||
return pubKey;
|
||||
}
|
||||
|
||||
auto
|
||||
@@ -398,6 +470,7 @@ makeResponse(
|
||||
beast::IP::Address public_ip,
|
||||
beast::IP::Address remote_ip,
|
||||
uint256 const& sharedValue,
|
||||
uint256 const& ekm,
|
||||
std::optional<std::uint32_t> networkID,
|
||||
ProtocolVersion protocol,
|
||||
Application& app)
|
||||
@@ -419,7 +492,8 @@ makeResponse(
|
||||
app.config().TX_REDUCE_RELAY_ENABLE,
|
||||
app.config().VP_REDUCE_RELAY_ENABLE));
|
||||
|
||||
buildHandshake(resp, sharedValue, networkID, public_ip, remote_ip, app);
|
||||
buildHandshake(
|
||||
resp, sharedValue, ekm, networkID, public_ip, remote_ip, app);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,23 @@ using http_request_type =
|
||||
using http_response_type =
|
||||
boost::beast::http::response<boost::beast::http::dynamic_body>;
|
||||
|
||||
/** Returns a value shared by the two endpoints of a TLS-secured connection.
|
||||
|
||||
This value is generated in a secure fashion and is never communicated over
|
||||
the wire, even over an encrypted connection. Used properly, it can help to
|
||||
detect and prevent preventing active MITM attacks.
|
||||
|
||||
@param ssl the SSL/TLS connection state.
|
||||
@param instance a 64-bit cookie, used in computing the shared value.
|
||||
@param outgoing true to return the "outgoing" value is needed; false for
|
||||
the "incoming" value.
|
||||
|
||||
@return On success, the 256-bit value this side believes both endpoints
|
||||
share; an unseated optional otherwise.
|
||||
*/
|
||||
[[nodiscard]] std::optional<uint256>
|
||||
getSessionEKM(stream_type& ssl, std::uint64_t instance, bool outgoing);
|
||||
|
||||
/** Computes a shared value based on the SSL connection state.
|
||||
|
||||
When there is no man in the middle, both sides will compute the same
|
||||
@@ -60,32 +77,61 @@ using http_response_type =
|
||||
std::optional<uint256>
|
||||
makeSharedValue(stream_type& ssl, beast::Journal journal);
|
||||
|
||||
/** Insert fields headers necessary for upgrading the link to the peer protocol.
|
||||
/** Populate header fields needed when upgrading the link to the peer protocol.
|
||||
|
||||
Some of the fields are used in critical security checks that can prevent
|
||||
active MITM attacks and ensure that the remote peer has the private keys
|
||||
that correspond to the public identity it claims.
|
||||
|
||||
@param h the list of HTTP headers fields to send.
|
||||
@param sharedValue a 256-bit value derived from the SSL session (legacy).
|
||||
@param ekm a 256-bit value derived from the SSL session.
|
||||
@param networkID the identifier of the network the server is configured for.
|
||||
@param public_ip The server's public IP.
|
||||
@param remote_ip The IP to which the server attempted to connect.
|
||||
@param app The main application object.
|
||||
|
||||
@note The `sharedValue` parameter is deprecated and will be removed in a
|
||||
future version of the code. It is replaced by `ekm` which is derived
|
||||
in a more standardized function.
|
||||
|
||||
\sa makeSharedValue, getSessionEKM
|
||||
*/
|
||||
void
|
||||
buildHandshake(
|
||||
boost::beast::http::fields& h,
|
||||
uint256 const& sharedValue,
|
||||
uint256 const& ekm,
|
||||
std::optional<std::uint32_t> networkID,
|
||||
beast::IP::Address public_ip,
|
||||
beast::IP::Address remote_ip,
|
||||
Application& app);
|
||||
|
||||
/** Validate header fields necessary for upgrading the link to the peer
|
||||
protocol.
|
||||
/** Validate header fields needed when upgrading the link to the peer protocol.
|
||||
|
||||
This performs critical security checks that ensure that prevent
|
||||
MITM attacks on our peer-to-peer links and that the remote peer
|
||||
has the private keys that correspond to the public identity it
|
||||
claims.
|
||||
Some of the fields are used in critical security checks that can prevent
|
||||
active MITM attacks and ensure that the remote peer has the private keys
|
||||
that correspond to the public identity it claims.
|
||||
|
||||
@return The public key of the remote peer.
|
||||
@throw A class derived from std::exception.
|
||||
@param h the list of HTTP headers fields we received.
|
||||
@param sharedValue a 256-bit value derived from the SSL session (legacy).
|
||||
@param ekm a 256-bit value derived from the SSL session.
|
||||
@param networkID the identifier of the network the server is configured for.
|
||||
@param public_ip The server's public IP.
|
||||
@param remote_ip The IP to which the server attempted to connect.
|
||||
@param app The main application object.
|
||||
|
||||
@return The public key of the remote peer on success. An exception
|
||||
otherwise.
|
||||
|
||||
@throw A class derived from std::exception, with an appropriate error
|
||||
message.
|
||||
*/
|
||||
PublicKey
|
||||
[[nodiscard]] PublicKey
|
||||
verifyHandshake(
|
||||
boost::beast::http::fields const& headers,
|
||||
uint256 const& sharedValue,
|
||||
uint256 const& ekm,
|
||||
std::optional<std::uint32_t> networkID,
|
||||
beast::IP::Address public_ip,
|
||||
beast::IP::Address remote,
|
||||
@@ -117,6 +163,7 @@ makeRequest(
|
||||
@param public_ip server's public IP
|
||||
@param remote_ip peer's IP
|
||||
@param sharedValue shared value based on the SSL connection state
|
||||
@param ekm shared value based on the SSL connection state
|
||||
@param networkID specifies what network we intend to connect to
|
||||
@param version supported protocol version
|
||||
@param app Application's reference to access some common properties
|
||||
@@ -129,6 +176,7 @@ makeResponse(
|
||||
beast::IP::Address public_ip,
|
||||
beast::IP::Address remote_ip,
|
||||
uint256 const& sharedValue,
|
||||
uint256 const& ekm,
|
||||
std::optional<std::uint32_t> networkID,
|
||||
ProtocolVersion version,
|
||||
Application& app);
|
||||
|
||||
@@ -248,11 +248,27 @@ OverlayImpl::onHandoff(
|
||||
return handoff;
|
||||
}
|
||||
|
||||
auto const ekm = getSessionEKM(*stream_ptr, app_.instanceID(), true);
|
||||
|
||||
if (!ekm)
|
||||
{
|
||||
m_peerFinder->on_closed(slot);
|
||||
handoff.moved = false;
|
||||
handoff.response = makeErrorResponse(
|
||||
slot,
|
||||
request,
|
||||
remote_endpoint.address(),
|
||||
"Session EKM is unavailable");
|
||||
handoff.keep_alive = false;
|
||||
return handoff;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
auto publicKey = verifyHandshake(
|
||||
request,
|
||||
*sharedValue,
|
||||
*ekm,
|
||||
setup_.networkID,
|
||||
setup_.public_ip,
|
||||
remote_endpoint.address(),
|
||||
@@ -677,8 +693,7 @@ OverlayImpl::crawlShards(bool includePublicKey, std::uint32_t relays)
|
||||
if (auto shardStore = app_.getShardStore())
|
||||
{
|
||||
if (includePublicKey)
|
||||
jv[jss::public_key] =
|
||||
toBase58(TokenType::NodePublic, app_.nodeIdentity().first);
|
||||
jv[jss::public_key] = app_.getNodePublicIdentity();
|
||||
|
||||
auto const shardInfo{shardStore->getShardInfo()};
|
||||
if (!shardInfo->finalized().empty())
|
||||
|
||||
@@ -603,6 +603,7 @@ PeerImp::fail(std::string const& reason)
|
||||
JLOG(journal_.warn()) << (n.empty() ? remote_address_.to_string() : n)
|
||||
<< " failed: " << reason;
|
||||
}
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
@@ -758,6 +759,7 @@ void
|
||||
PeerImp::doAccept()
|
||||
{
|
||||
assert(read_buffer_.size() == 0);
|
||||
assert(!inbound_);
|
||||
|
||||
JLOG(journal_.debug()) << "doAccept: " << remote_address_;
|
||||
|
||||
@@ -768,6 +770,11 @@ PeerImp::doAccept()
|
||||
if (!sharedValue)
|
||||
return fail("makeSharedValue: Unexpected failure");
|
||||
|
||||
auto const ekm = getSessionEKM(*stream_ptr_, app_.instanceID(), !inbound_);
|
||||
|
||||
if (!ekm)
|
||||
return fail("getSessionEKM: Unexpected failure");
|
||||
|
||||
JLOG(journal_.info()) << "Protocol: " << to_string(protocol_);
|
||||
JLOG(journal_.info()) << "Public Key: "
|
||||
<< toBase58(TokenType::NodePublic, publicKey_);
|
||||
@@ -795,6 +802,7 @@ PeerImp::doAccept()
|
||||
overlay_.setup().public_ip,
|
||||
remote_address_.address(),
|
||||
*sharedValue,
|
||||
*ekm,
|
||||
overlay_.setup().networkID,
|
||||
protocol_,
|
||||
app_);
|
||||
|
||||
@@ -251,26 +251,7 @@ sign(PublicKey const& pk, SecretKey const& sk, Slice const& m)
|
||||
case KeyType::secp256k1: {
|
||||
sha512_half_hasher h;
|
||||
h(m.data(), m.size());
|
||||
auto const digest = sha512_half_hasher::result_type(h);
|
||||
|
||||
secp256k1_ecdsa_signature sig_imp;
|
||||
if (secp256k1_ecdsa_sign(
|
||||
secp256k1Context(),
|
||||
&sig_imp,
|
||||
reinterpret_cast<unsigned char const*>(digest.data()),
|
||||
reinterpret_cast<unsigned char const*>(sk.data()),
|
||||
secp256k1_nonce_function_rfc6979,
|
||||
nullptr) != 1)
|
||||
LogicError("sign: secp256k1_ecdsa_sign failed");
|
||||
|
||||
unsigned char sig[72];
|
||||
size_t len = sizeof(sig);
|
||||
if (secp256k1_ecdsa_signature_serialize_der(
|
||||
secp256k1Context(), sig, &len, &sig_imp) != 1)
|
||||
LogicError(
|
||||
"sign: secp256k1_ecdsa_signature_serialize_der failed");
|
||||
|
||||
return Buffer{sig, len};
|
||||
return signDigest(pk, sk, sha512_half_hasher::result_type(h));
|
||||
}
|
||||
default:
|
||||
LogicError("sign: invalid type");
|
||||
|
||||
@@ -1100,6 +1100,7 @@ struct LedgerReplayer_test : public beast::unit_test::suite
|
||||
addr,
|
||||
addr,
|
||||
uint256{1},
|
||||
uint256{1},
|
||||
1,
|
||||
{1, 0},
|
||||
serverEnv.app());
|
||||
|
||||
@@ -511,6 +511,7 @@ public:
|
||||
addr,
|
||||
addr,
|
||||
uint256{1},
|
||||
uint256{1},
|
||||
1,
|
||||
{1, 0},
|
||||
env->app());
|
||||
|
||||
@@ -1534,6 +1534,7 @@ vp_squelched=1
|
||||
addr,
|
||||
addr,
|
||||
uint256{1},
|
||||
uint256{1},
|
||||
1,
|
||||
{1, 0},
|
||||
env_.app());
|
||||
|
||||
Reference in New Issue
Block a user